Skip to content

List every finding in one pull request comment - #2

Merged
tauanbinato merged 9 commits into
mainfrom
v1.2
Sep 28, 2026
Merged

tauanbinato merged 9 commits into
mainfrom
v1.2

Conversation

@tauanbinato

@tauanbinato tauanbinato commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Merge after JevGate 0.26.0 is released, checking two names it will define: api-key-kind gives the key to JevGate as OPENROUTER_API_KEY or AI_GATEWAY_API_KEY (the 0.26 provider item's names, with the environment read TypeSafe, then OpenRouter, then Vercel), and the comment reads each finding's gate (fails, measuring, advisory, the 0.26 maturity item's field). If either changes, adjust the case in check.sh and test/key-kinds.sh, or FAILS/MEASURING in render.cjs. The comment itself works with any JevGate version, 0.25.0 included. Then release as v1.2.0 and move v1.

What changes

  • One pull request comment listing every finding (new input comment, on by default). GitHub shows at most 10 error and 10 warning annotations per step, so larger runs lost findings from the diff. The comment is updated in place on each run and has:
    • the gate's result and JevGate's reasons, never recomputed (they depend on fail_on, [[scope]] globs and, from 0.26, the maturity default);
    • the run's files, API requests, input tokens and cost ($0.042 per million input tokens when the answering model is jev-1.13.0; "cost unknown" when requests were answered with no counted tokens, as a gateway without usage would, never $0);
    • every finding nobody accepted, by level (reviews open, considers collapsed past 10, notes collapsed), then by file in path order, then by line, each linked to its line at the checked commit;
    • from JevGate 0.26.0, "(fails the gate)" after the rule of each finding that fails it, and one line counting the findings reported without failing it while their rules and levels are still being measured (the report's per-finding gate, as JevGate's agent text shows it); older reports carry no such field and nothing is marked;
    • for an incomplete run (exit 2), a [!CAUTION] alert first: "JevGate could not finish this run (exit code 2). The gate was not applied, and findings may be missing.", then each distinct reason with the number of files it stopped (TypeSafe HTTP 402; request was not retried (1 file)). A run that stopped before writing a report still gets the alert.
  • How it is built: render.cjs writes the comment from .jevgate/latest.json alone; comment.cjs posts it through actions/github-script (v9.0.0, pinned by SHA, node24 like actions/cache v6.1.0). Chosen over bash with jq or Python: GitHub's hosted images have both (jq 1.7 to 1.8; Python 3.12 to 3.14, documented as python only on Windows), but self-hosted runners need not, while every runner has the Node that runs JavaScript actions; and grouping, escaping and a byte cap are far easier to write and test in JavaScript. gh is also missing on some self-hosted runners.
  • check.sh: saves this run's report for the comment before the SARIF replay replaces it (the replay sends no request, so its report shows no cost), and only when the check wrote a new one (cksum before and after; a usage error exits 2 and leaves the previous report, with a generated_at in the same second). It also records the checked commit, the working directory's prefix in the repository (for links in monorepos) and jevgate --version.
  • Sticky and safe: a hidden first line (<!-- jevgate-action comment key=<job>[ <dir>] run=<run id> -->) finds the comment again. Only bot comments starting with it are edited. A run for an older push leaves a newer run's comment alone; copies left by two first runs racing are deleted, keeping the oldest. Text from the report quotes the change's code, so outside code spans HTML, links, mentions and markers are escaped, and an unclosed backtick cannot pair with the next field's code span (checked with GitHub's own Markdown renderer).
  • Size: at most 65,536 UTF-8 bytes (never fewer than GitHub's character count). Notes are cut first, then the lowest-ranked considers, with a line saying how many of each are left out.
  • Refused token: without pull-requests: write, and on pull requests from forks, the step logs a warning and a job-summary paragraph saying why and carries on. Any other failure is a warning too: the comment never fails the job.
  • New input api-key-kind (typesafe, openrouter, vercel; default typesafe): the key goes to JevGate in that kind's variable and no other kind's, so a key the job keeps for its own tests (OPENROUTER_API_KEY is common) is never spent, and a TYPESAFE_API_KEY in the job's env cannot win over the kind chosen here (JevGate 0.26 reads it first). With a gateway kind and JevGate older than 0.26.0, the check stops with an error naming the version. An empty api-key no longer blanks a key set in the job's env.
  • From JevGate 0.28.0 (e9352c3, from 0.28's tested patch): each finding ends as JevGate's own output ends it, with how often findings of its rule and level were right on projects JevGate was never tuned on ("Right 87% of the time (23 labels).", or "Not yet measured." below 20 labels), read from the report's per-finding precision; 0.28 no longer puts the probability in the message. Older reports show nothing more.
  • base input wording (4417811): from JevGate 0.26.0 a base means only the changed lines of changed files (--whole-files in args for whole files); the description said changed files.
  • Fixes: exit-code is written when the base revision is missing (it was empty exactly when the run was incomplete), and the report output is a Windows path on Windows (Git Bash's $PWD is /d/a/..., which Node, and so upload-artifact, resolves to D:\d\a\...).

Decisions to review

  1. comment defaults to true. Existing @v1 workflows with only contents: read get one warning per run until they add pull-requests: write or set comment: false. The README examples now grant it.
  2. One comment per job id and working directory; matrix jobs share it (last writer wins, same run id).
  3. The input name api-key-kind, next to api-key, and the variable names it sets. JevGate 0.26's auth login names the same choice --provider (same three values); provider would also fit here.
  4. The cost rate lives in the action too (render.cjs), as in JevGate's output.rs. If JevGate adds a cost field to the report, the action should read it.
  5. Which findings fail the gate comes only from the report's per-finding gate (0.26.0 on), never recomputed: that would need JevGate's scope globs and the maturity default. With older versions the comment shows the gate's reasons and no per-finding mark; the annotations still say error or warning.

Measured (no Jev requests)

Rendering the largest corpus reports (whole-repository runs, far larger than a pull request's):

Report Findings (review/consider/note) Comment bytes Shown Render
JevGate's own --base run 1 / 1 / 11 4,580 all <1 ms
cookiecutter-django 5 / 20 / 77 43,376 all 3 ms
bend 22 / 96 / 602 64,904 22 / 96 / 45 13 ms
b2-bend-collections 54 / 89 / 1,604 65,091 54 / 80 / 0 21 ms
b3-pi-fabric (28 MB) 107 / 257 / 1,053 64,645 107 / 18 / 0 18 ms

Tests

  • node --test test/render.test.cjs test/comment.test.cjs (30 tests, Node's built-in runner, no dependencies): renders JevGate's own --base report and 0.25.0 reports of a run without a key and after a 402; the cap on a synthetic 3,060-finding report; escaping; and posting against an in-memory issues API (create, update, unchanged, duplicates, older run, 403, fork, 500, unreadable report, not a pull request, dry run). Thirteen mutations of the risky lines (escaping, the run guard, the author check, the cap, refusals, the gate marks) each fail a test.
  • bash test/key-kinds.sh: check.sh against a stand-in jevgate that records the key variables it gets, for every kind, old and new versions, and keys already in the job's env.
  • CI, all free: unit (the above on Linux, macOS, Windows); fixture (four runners: a repository created in the job whose one change no cache answers, so --cache-only writes a report and exits 2 with no key; then a passing check whose comment the read-only token cannot post must not fail; the report output must open from Node); comment (pull requests from this repository, with pull-requests: write: two checks in one job must leave exactly one comment, holding the second's incomplete banner, so this pull request shows a live one).

Known and left alone

args: --cache-only together with sarif-file makes the replay pass --cache-only twice, which fails with a warning that wrongly says 0.18.0 is needed. It predates this change.

After merging

  • Tag v1.2.0 with the notes below, and move v1.
  • In jevgate: site/src/ci.md shows permissions: contents: read only; add pull-requests: write and a sentence on the comment. JevGate's own .github/workflows/jevgate.yml pins v1.0.0 by SHA.
  • Bump version: in the README example when 0.26.0 ships.

Release notes for v1.2.0

jevgate-action 1.2.0

On pull requests, the action lists every finding in one comment and updates it on each run: GitHub shows at most 10 error and 10 warning annotations per step. The comment gives the gate's result, the run's API requests, input tokens and cost, and the findings by level and file, linked to their lines, with JevGate 0.26.0's mark on each finding that fails the gate and, from 0.28.0, how often findings like it were right; a run that could not finish (no key, HTTP 402, the request budget) opens with a caution and the reasons. It needs `pull-requests: write`; where the token can't comment, as on pull requests from forks, the run says so and carries on. `comment: false` turns it off.

New input `api-key-kind` (`typesafe`, `openrouter` or `vercel`) takes OpenRouter and Vercel AI Gateway keys; the gateways need JevGate 0.26.0 or later.

Fixes: `exit-code` is set when the base revision is missing, and the `report` output is a Windows path on Windows runners.

check.sh exited 2 before writing its outputs when the base revision was not
in the checkout, so the documented exit-code output was empty exactly when
the run was incomplete. Every exit now goes through one function that writes
exit-code and report first.
On Windows runners the bash shell is Git Bash, whose $PWD is /d/a/...; the
report output built from it names a path Node resolves to D:\d\a\..., so
upload-artifact and other JavaScript actions could not open it. cygpath -m
turns it into D:/a/..., which both bash and Node open; elsewhere the path is
unchanged.

A new job checks it on all four runners: a repository created in the job,
whose one change no cache answers, runs with --cache-only, which writes a
report and ends incomplete with no key and nothing sent. It checks exit code
2 and that Node can open the report output.
JevGate 0.26.0 accepts keys from the two gateways that serve Jev. The new
input api-key-kind (typesafe, openrouter or vercel; typesafe by default)
says which service issued api-key, and the check gets the key as
TYPESAFE_API_KEY, OPENROUTER_API_KEY or AI_GATEWAY_API_KEY, and no other
kind's key: a job that keeps an OpenRouter key for its own tests must not
have JevGate spend it because it also reads that variable. With a gateway
kind and a JevGate older than 0.26.0, the check stops with an error naming
the version, instead of JevGate reporting that no key is configured.

The key is set only when api-key is given, so a key a workflow puts in the
job's env for that kind is no longer replaced by an empty input.

test/key-kinds.sh runs check.sh against a stand-in jevgate that records the
key variables it receives, on Linux, macOS and Windows.
GitHub shows at most 10 error and 10 warning annotations per step, so a
pull request with more findings showed only some of them on the diff. On
pull request events the action now posts one comment and updates it on each
run: the gate's result and reasons, the run's files, API requests, input
tokens and cost, then every finding nobody accepted, by level (reviews open,
considers collapsed past 10, notes collapsed) and by file, each linked to
its line at the checked commit. A run that could not finish (exit 2: no
key, a provider error such as HTTP 402, the request budget) opens with a
caution alert and each distinct reason with the files it stopped.

- render.cjs writes it from the JSON report alone, so it works with any
  JevGate version, and comment.cjs posts it, run by actions/github-script
  pinned by SHA on any runner that runs JavaScript actions. check.sh saves
  this run's report before the SARIF replay replaces it (the replay sends no
  request, so its report shows no cost), and only when the check wrote a new
  one (checksum before and after), never a report left from an earlier check.
- A hidden first line keys the comment by job and working directory, and
  names the run that wrote it: a run for an older push leaves a newer run's
  comment alone, and copies left by two first runs racing are deleted.
  Only bot comments starting with the marker are edited.
- The body stays under GitHub's 65,536-character limit, measured in UTF-8
  bytes: notes are cut first, then the lowest-ranked considers, with a line
  saying how many of each are left out. On the largest corpus reports
  (1,500 to 1,750 findings of whole-repository runs) it keeps every review
  and fills 64,645 to 65,091 bytes in under 30 ms.
- Text from the report quotes the change's code, so outside code spans it
  is escaped: no HTML, links, mentions or comment markers, and an unclosed
  backtick cannot pair with the next field's code span.
- Without pull-requests: write, as on pull requests from forks, it says so
  in the log and the job summary and the step passes; any other failure is
  a warning too. comment: false turns it off.

Tested with node --test (renders JevGate's own --base report and 0.25.0
reports of a run without a key and after a 402, a fake issues API for
create, update, duplicates, older runs and refusals) and in CI: on four
runners a read-only token's refused comment must not fail a passing check,
and on pull requests from this repository two checks in one job must leave
one comment holding the second's incomplete banner.
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

JevGate: run incomplete

Caution

JevGate could not finish this run (exit code 2). The gate was not applied, and findings may be missing.

  • No current cached response; rerun without --cache-only to allow an API request (1 file)

1 file · 0 API requests · 0 input tokens

jevgate 0.30.0 · commit 0783863 · workflow run · updated on each run

From 0.26.0 JevGate blocks by default only on rules and levels measured
right at least 80% of the time on unseen projects; the others are reported
without failing the gate, and each finding records how the gate counted it
(`gate`: fails, measuring or advisory). Without that, the comment would list
five reviews under "gate passed" with nothing to say why.

A finding that fails the gate gets "(fails the gate)" after its rule, as in
JevGate's agent text, and one line counts the findings reported without
failing it because their rules and levels are still being measured. Reports
from earlier versions have no such field, and nothing is marked.
JevGate 0.28 removes the probability from each finding's message and
records instead how often findings of its rule and level were right on
projects it was never tuned on (`precision`: `right` of `labeled`). The
comment listed only the message and the next step, so with 0.28 it
would show neither. Each finding now ends as JevGate's own output ends
it: "Right 87% of the time (23 labels)." or, below 20 labels, "Not yet
measured."; a report before 0.28 shows nothing more.
JevGate 0.26 judges only what a change touches when given --base: the
changed lines of changed files, with --whole-files for the old
behavior. The input's description still said it reviewed changed files.
…ommitted cache

From JevGate 0.30 a finding of its own rules in a preview language's file
carries `preview` with the language, and its `precision` holds that
language's counts. The comment rebuilt the precision sentence without it
("Right 12% of the time (34 labels)." for a Bash shared-logic review,
where JevGate says "in Bash") and explained every `measuring` finding as
a rule still being measured, wrong for a C function-simplification
review, whose rule and level fail by default outside preview languages.
It now says "in <language>", and gives preview findings their own reason:
the language is in preview, and JevGate's own rules never fail the
default gate there. A law finding keeps JevGate's caveat that it was
labeled only on Bend 2 projects.

A pull request could also commit answers under .jevgate/cache that clear
its own code; JevGate 0.28 and later ignores cache files Git tracks, and
the action now removes a checked-out cache before restoring its own, for
earlier versions too.
The first example pinned 0.25.0 and the gateway one 0.26.0. 0.30.0 is the
release this version of the action words its comment for (preview
languages, per-language precision), and the gateways need 0.26.0 or later.
@tauanbinato
tauanbinato merged commit 49d157b into main Sep 28, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant